package edu.um.analyzer;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import edu.um.analyzer.exception.DuplicatedDeclarationException;
import edu.um.analyzer.exception.InvalidArrayIndexException;
import edu.um.analyzer.exception.InvalidVariableReferenceException;
import edu.um.analyzer.exception.TypeMismatchException;
import edu.um.analyzer.exception.VariableNotDeclaredException;
import edu.um.automata.StackAutomata;
import edu.um.automata.exception.InvalidEndingStateException;
import edu.um.automata.exception.StateNotFoundException;
import edu.um.automata.exception.TransitionNotFoundException;
import edu.um.expression.Expression;
import edu.um.expression.ExpressionTreeNodeFactory;
import edu.um.expression.TwoTermsValidation;
import edu.um.expression.Validation;
import edu.um.expression.ValidationStrategy;
import edu.um.io.Console;
import edu.um.token.Token;
import edu.um.token.TokenKind;
import edu.um.token.Tokenizer;
import edu.um.token.exception.InvalidTokenException;
import edu.um.variable.ArrayVariable;
import edu.um.variable.Range;
import edu.um.variable.RecordVariable;
import edu.um.variable.Variable;
import edu.um.variable.VariableType;

public class Analyzer {

  private final InputStream stream;

  private final List<Expression> tokenizedSentences = new ArrayList<Expression>();

  private final Map<String, Variable> variableMap = new HashMap<String, Variable>();

  private final List<Validation> validations = new ArrayList<Validation>();

  public Analyzer(String fileName) throws FileNotFoundException {
    stream = new FileInputStream(fileName);
    ExpressionTreeNodeFactory.setVariablemap(variableMap);
  }

  public void run() throws IOException {

    try {
      this.performLexicalAnalysis();
      this.performSyntacticAnalysis();
      performSemanticAnalysis();
      checkForUnusedVariables();

    } catch(Exception e) {
      Console.log("EROR -> " + e.getMessage());
      if (Console.isDebug()) {
        e.printStackTrace();
      }
    }
  }

  /**
   *
   * @throws IOException
   * @throws InvalidTokenException
   */
  private void performLexicalAnalysis() throws IOException, InvalidTokenException {
    Console.log("Lexical Analysis");
    try {
      Tokenizer tokenizer = new Tokenizer(stream);
      Expression newSentence = new Expression();
      tokenizedSentences.add(newSentence);
      boolean isInRecord = false;
      while (tokenizer.hasMoreToken()) {
        Token token = tokenizer.nextToken();
        Console.debug("New Token: " + token);
        TokenKind kind = token.getKind();
        if (TokenKind.TIPO_RECORD == kind) {
          isInRecord = true;
        }
        if (TokenKind.END == kind) {
          isInRecord = false;
        }
        newSentence.add(token);
        if (TokenKind.BEGIN == kind
            || (!isInRecord && TokenKind.FIN_SENTENCIA == kind)) {
          newSentence = new Expression();
          tokenizedSentences.add(newSentence);
        }
      }
      Console.log("OK");
    } finally {
      if (stream != null) {
        try {
          stream.close();
        } catch(IOException e) {
          e.printStackTrace();
        }
      }
    }
  }

  /**
   * Performs the sintactical analysis.
   *
   * @throws StateNotFoundException
   * @throws TransitionNotFoundException
   * @throws InvalidEndingStateException
   */
  private void performSyntacticAnalysis() throws StateNotFoundException,
      TransitionNotFoundException, InvalidEndingStateException {
    Console.log("Sintactical Analysis");
    StackAutomata automata = new StackAutomata();
    Token token = null;
    for (Expression sentence : tokenizedSentences) {
      Console.debug("Process sentence: " + sentence);
      Iterator<Token> iterator = sentence.iterator();
      while (iterator.hasNext()) {
        token = iterator.next();
        Console.debug("Process Token: " + token);
        automata.process(token.getKind());
      }
    }
    if (!automata.isInAcceptableState()) {
      throw new InvalidEndingStateException(token);
    }
    Console.log("OK");
    Console.debug("Variables: " + variableMap.entrySet());
    Console.debug("Validations : " + validations.size());
    Console.debug(validations);
  }

  /**
   * @throws InvalidVariableReferenceException
   * @throws TypeMismatchException
   * @throws VariableNotDeclaredException
   * @throws InvalidArrayIndexException
   * @throws DuplicatedDeclarationException
   *
   */
  private void performSemanticAnalysis() throws InvalidVariableReferenceException, TypeMismatchException, InvalidArrayIndexException, VariableNotDeclaredException, DuplicatedDeclarationException {
    Console.log("Semantic Analysis");
    addVariablesAndValidations();
    for (Validation validation : validations) {
      validation.run();
    }
    Console.log("OK");
  }

  /**
   * @throws InvalidVariableReferenceException
   * @throws VariableNotDeclaredException
   * @throws InvalidArrayIndexException
   * @throws DuplicatedDeclarationException
   *
   */
  private void addVariablesAndValidations() throws InvalidVariableReferenceException, InvalidArrayIndexException, VariableNotDeclaredException, DuplicatedDeclarationException {
    for (Expression expression : tokenizedSentences) {
      addVariablesAndValidations(expression);
    }
  }

  /**
   *
   * @param expression
   * @throws InvalidVariableReferenceException
   * @throws VariableNotDeclaredException
   * @throws InvalidArrayIndexException
   * @throws DuplicatedDeclarationException
   */
  private void addVariablesAndValidations(Expression expression) throws InvalidVariableReferenceException, InvalidArrayIndexException, VariableNotDeclaredException, DuplicatedDeclarationException {
    TokenKind firstTokenKind = expression.get(0).getKind();
    if (TokenKind.VAR == firstTokenKind) {
      AddToVaribleMap(expression);
    }
    if (TokenKind.IF == firstTokenKind) {
      int index = expression.indexOf(TokenKind.THEN);
      addIfSentenceValidation(expression.subExpression(1, index));
      addVariablesAndValidations(new Expression(
          expression.subList(index + 1, expression.size())));
    }
    if (TokenKind.ID == firstTokenKind) {
      addAsignationValidation(expression);
    }
    if (TokenKind.WRITELN == firstTokenKind ||
        TokenKind.READLN == firstTokenKind) {
      addCommandValidation(expression);

    }
  }

  /**
   *
   */
  private void checkForUnusedVariables() {
    for (String name : variableMap.keySet()) {
      Variable variable = variableMap.get(name);
      if (!variable.isUsed()) {
        Console.log("Variable " + variable.getName()
            + " is never used");
      }
    }
  }

  /**
   *
   * @param sentence
   * @throws DuplicatedDeclarationException
   */
  private void AddToVaribleMap(Expression sentence) throws DuplicatedDeclarationException {
    String name = sentence.get(1).getValue();
    if (variableMap.get(name) != null) {
      throw new DuplicatedDeclarationException(sentence.get(1));

    }
    TokenKind kind = sentence.get(3).getKind();
    if (kind == TokenKind.TIPO_STRING_BLANCO
        || kind == TokenKind.TIPO_STRING_SUBRAYADO
        || kind == TokenKind.TIPO_WORD
        || kind == TokenKind.TIPO_BYTE) {
      variableMap.put(name, new Variable(name, kind.getVariableType()));
    }
    if (kind == TokenKind.TIPO_ARRAY) {
      List<Range> ranges = new ArrayList<Range>();
      int index = 4;
      while (sentence.get(index).getKind() != TokenKind.CIERRA_CORCHETE) {
        Range range = new Range(
            Integer.parseInt(sentence.get(index + 1).getValue()), Integer
                .parseInt(sentence.get(index + 3).getValue()));
        ranges.add(range);
        index += 4;
      }
      VariableType subType = sentence.get(index + 2).getKind()
          .getVariableType();
      variableMap.put(name, new ArrayVariable(name, subType, ranges));
    }
    if (kind == TokenKind.TIPO_RECORD) {
      List<Variable> variables = new ArrayList<Variable>();
      int index = 4;
      Token token = sentence.get(index);
      while (token.getKind() != TokenKind.END) {
        Variable variable = new Variable(token.getValue(),
            sentence.get(index + 2).getKind().getVariableType());
        if (variables.contains(variable)) {
          throw new DuplicatedDeclarationException(token);
        }
        variables.add(variable);
        index += 4;
        token = sentence.get(index);
      }
      variableMap.put(name, new RecordVariable(name, variables));

    }
  }

  /**
   *
   * @param expression
   * @throws InvalidVariableReferenceException
   * @throws VariableNotDeclaredException
   * @throws InvalidArrayIndexException
   */
  public void addAsignationValidation(Expression expression) throws InvalidVariableReferenceException, InvalidArrayIndexException, VariableNotDeclaredException {
    int index = expression.indexOf(TokenKind.ASIGNA_VALOR);
    Expression expression1 = new Expression(expression.subList(0, index));
    Expression expression2 = new Expression(expression.subList(index + 1,
        expression.size() - 1));

    validations.add(new TwoTermsValidation(ExpressionTreeNodeFactory
        .createExpressionTreeNode(expression1), ExpressionTreeNodeFactory
        .createExpressionTreeNode(expression2), ValidationStrategy.ASIGNATION));
  }

  /**
   *
   * @param expression
   * @throws InvalidVariableReferenceException
   * @throws VariableNotDeclaredException
   * @throws InvalidArrayIndexException
   */
  public void addIfSentenceValidation(Expression expression) throws InvalidVariableReferenceException, InvalidArrayIndexException, VariableNotDeclaredException {
    int index = expression.indexOf(TokenKind.COMPARADOR);
    Expression expression1 = new Expression(expression.subList(0, index));
    Expression expression2 = new Expression(expression.subList(index + 1,
        expression.size()));

    validations
        .add(new TwoTermsValidation(ExpressionTreeNodeFactory
            .createExpressionTreeNode(expression1), ExpressionTreeNodeFactory
            .createExpressionTreeNode(expression2),
            ValidationStrategy.COMPARATION));
  }

  /**
   *
   * @param sentence
   * @throws InvalidVariableReferenceException
   * @throws VariableNotDeclaredException
   * @throws InvalidArrayIndexException
   */
  public void addCommandValidation(Expression sentence) throws InvalidVariableReferenceException, InvalidArrayIndexException, VariableNotDeclaredException {
    Expression expression = sentence.subExpression(2, sentence.size() - 2);
    validations.add(new Validation(ExpressionTreeNodeFactory
        .createExpressionTreeNode(expression)));
  }

  public static void main(String[] args) throws IOException {
    int parametersNumber = args.length;
    if (parametersNumber == 0) {
      Console.log("Especifique el archivo");
      return;
    }
    if (parametersNumber > 1) {
      if ("debug".equals(args[1])) {
        Console.setDebug(true);
      }
    }
    Analyzer analyzer = new Analyzer(args[0]);
    analyzer.run();
  }
}
